package com.epam.training.collection;

import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;

public class MyHashMap<K, V> implements MyMap<K, V> {

	private float loadFactor;
	private int threshold;

	private int size;
	private SimpleEntry<K, V>[] table;

	private static final int DEFAULT_CAPACITY = 16;
	private static final float DEFAULT_LOAD_FACTOR = 0.75f;

	MyHashMap() {
		this(DEFAULT_CAPACITY);
	}

	MyHashMap(int initialCapacity) {
		this(initialCapacity, DEFAULT_LOAD_FACTOR);
	}

	@SuppressWarnings("unchecked")
	MyHashMap(int initialCapacity, float loadFactor) {

		if (initialCapacity < 0 || loadFactor <= 0) {
			throw new IllegalArgumentException();
		}

		size = 0;
		this.loadFactor = loadFactor;
		threshold = (int) (initialCapacity * loadFactor);
		table = new SimpleEntry[initialCapacity];
	}

	private static class SimpleEntry<K, V> implements MyMap.Entry<K, V> {

		final int hash;
		final K key;
		V value;
		SimpleEntry<K, V> next;

		SimpleEntry(int hash, K key, V value, SimpleEntry<K, V> next) {
			this.hash = hash;
			this.key = key;
			this.value = value;

			this.next = next;
		}

		public K getKey() {
			return key;
		}

		public V getValue() {
			return value;
		}

		public V setValue(V value) {
			V old = this.value;
			this.value = value;

			return old;
		}

		public int hashCode() {
			return Objects.hashCode(key) ^ Objects.hashCode(value);
		}

		public boolean equals(Object obj) {
			if (obj == this)
				return true;

			if (obj instanceof Map.Entry) {
				Map.Entry<?, ?> e = (Map.Entry<?, ?>) obj;
				if (Objects.equals(key, e.getKey())
						&& Objects.equals(value, e.getValue()))
					return true;
			}
			return false;
		}
	}

	private int hash(Object key) {
		int h = key.hashCode();

		h ^= (h >>> 20) ^ (h >>> 12);

		return h ^ (h >>> 7) ^ (h >>> 4);
	}

	private int indexFor(int hash, int length) {
		return hash & (length - 1);
	}

	public void clear() {
		for (int i = 0; i < table.length; i++) {
			table[i] = null;
		}
		size = 0;
	}

	public boolean containsKey(K key) {
		return getSimpleEntry(key) != null;
	}

	public boolean containValue(V value) {
		if (size == 0) {
			return false;
		}

		for (int i = 0; i < table.length; i++) {
			for (SimpleEntry<K, V> e = table[i]; e != null; e = e.next) {
				if ((value == null && e.value == value)
						|| e.value.equals(value)) {
					return true;
				}
			}
		}

		return false;
	}

	public V get(K key) {
		final SimpleEntry<K, V> k = getSimpleEntry(key);

		return (k == null) ? null : k.value;
	}

	public boolean isEmpty() {
		return size == 0;
	}

	private SimpleEntry<K, V> getSimpleEntry(K key) {
		if (size == 0) {
			return null;
		}

		int hash = (key == null) ? 0 : hash(key);

		SimpleEntry<K, V> e = table[indexFor(hash, table.length)];

		while (e != null) {
			K k;
			if (e.hash == hash
					&& ((k = e.key) == key || (key != null && key.equals(k))))
				return e;

			e = e.next;
		}

		return null;
	}

	public V put(K key, V value) {
		if (key == null) {
			final SimpleEntry<K, V> n = table[0].next;
			table[0] = new SimpleEntry<K, V>(0, key, value, n);

			return n.value;
		}

		final int hash = hash(key);
		final int index = indexFor(hash, table.length);
		final SimpleEntry<K, V> entry = table[index];

		if (entry != null && entry.hash == hash
				&& (entry.key == key || key.equals(entry.key))) {
			V oldValue = entry.value;
			entry.value = value;

			return oldValue;
		}

		table[index] = new SimpleEntry<K, V>(hash, key, value, entry);
		size++;

		if (size >= threshold)
			resize(2 * table.length);

		return null;
	}

	@SuppressWarnings("unchecked")
	private void resize(int newCapacity) {

		if (newCapacity >= Integer.MAX_VALUE) {
			return;
		}

		SimpleEntry<K, V>[] newTable = new SimpleEntry[newCapacity];
		transfer(newTable);
		table = newTable;
		threshold = (int) (newCapacity * loadFactor);
	}

	private void transfer(SimpleEntry<K, V>[] newTable) {
		SimpleEntry<K, V>[] oldTable = table;
		int capacity = newTable.length;

		for (int i = 0; i < oldTable.length; i++) {
			SimpleEntry<K, V> e = oldTable[i];
			if (e != null) {
				oldTable[i] = null;
				do {
					SimpleEntry<K, V> next = e.next;
					int index = indexFor(e.hash, capacity);
					e.next = newTable[index];
					newTable[index] = e;
					e = next;
				} while (e != null);
			}
		}
	}

	public V remove(K key) {
		if (size == 0) {
			return null;
		}

		int hash = (key == null) ? 0 : hash(key.hashCode());
		int i = indexFor(hash, table.length);
		SimpleEntry<K, V> prev = table[i];
		SimpleEntry<K, V> e = prev;

		while (e != null) {
			SimpleEntry<K, V> next = e.next;
			K k;
			if (e.hash == hash
					&& ((k = e.key) == key || (key != null && key.equals(k)))) {
				size--;
				if (prev == e)
					table[i] = next;
				else
					prev.next = next;
				return e.value;
			}
			prev = e;
			e = next;
		}

		return null;
	}

	public int size() {
		return size;
	}

	public Iterator<MyMap.Entry<K, V>> entryIterator() {
		return new EntryIterator();
	}

	class EntryIterator implements Iterator<MyMap.Entry<K, V>> {
		SimpleEntry<K, V> next;
		int index;
		SimpleEntry<K, V> current;

		EntryIterator() {
			while (index < table.length && (next = table[index++]) == null)
				;
		}

		public boolean hasNext() {
			return next != null;
		}

		public Entry<K, V> next() {
			SimpleEntry<K, V> e = next;

			if (e == null)
				throw new NoSuchElementException();

			if ((next = e.next) == null) {
				while (index < table.length && (next = table[index++]) == null)
					;
			}
			current = e;
			return e;
		}

		public void remove() {
			if (current == null)
				throw new IllegalStateException();

			K k = current.key;
			current = null;

			MyHashMap.this.remove(k);
		}

	}
}
